#!/bin/dash
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# NOTE: This script works in dash, but is not as featureful.  Specifically,
# dash omits readline support (history & command line editing).  So we try
# to run through bash if it exists, otherwise we stick to dash.  All other
# code should be coded to the POSIX standard and avoid bashisms.
#
# Please test that any changes continue to work in dash by running
# '/build/$BOARD/bin/dash crosh --dash' before checking them in.

# Don't allow SIGHUP to terminate crosh. This guarantees that even if the user
# closes the crosh window, we make our way back up to the main loop, which gives
# cleanup code in command handlers a chance to run.
trap '' HUP

# Do not let CTRL+C kill crosh itself.  This does let the user kill commands
# that are run by crosh (like `ping`).
trap : INT

# If it exists, use $DATA_DIR to define the $HOME location, as a first step
# to entirely removing the use of $HOME. (Though HOME should be used as
# fall-back when DATA_DIR is unset.)
# TODO(keescook): remove $HOME entirely crbug.com/333031
if [ "${DATA_DIR+set}" = "set" ]; then
  export HOME="${DATA_DIR}/user"
fi

IS_BASH=0
try_bash() {
  # If dash was explicitly requested, then nothing to do.
  case " $* " in
  *" --dash "*) return 0;;
  esac

  # If we're already bash, then nothing to do.
  if type "history" 2>/dev/null | grep -q "shell builtin"; then
    IS_BASH=1
    return 0
  fi

  # Still here?  Relaunch in bash.
  exec /bin/bash $0 "$@"
}
try_bash "$@"

#
# Please keep the help text in alphabetical order!
#
HELP='
 exit
  Exit crosh.

 help
  Display this help.

 help_advanced
  Display the help for more advanced commands, mainly used for debugging.

 ping [-c count] [-i interval] [-n] [-s packetsize] [-W waittime] <destination>
  Send ICMP ECHO_REQUEST packets to a network host.  If <destination> is "gw"
  then the next hop gateway for the default route is used.

 ssh [optional args...]
  Starts the ssh subsystem if invoked without any arguments.
  "ssh <user> <host>", "ssh <user> <host> <port>", "ssh <user>@<host>",
  or "ssh <user>@<host> <port>" connect without entering the subsystem.

 ssh_forget_host
  Remove a host from the list of known ssh hosts.  This command displays
  a menu of known hosts and prompts for the host to forget.

 top
  Run top.
'

HELP_ADVANCED='
 battery_test [<test length>]
  Tests battery discharge rate for given number of seconds. No argument will
  default to 300s test.

 bt_console [<agent capability>]
  Enters a Bluetooth debugging console. Optional argument specifies the
  capability of a pairing agent the console will provide; see the Bluetooth
  Core specification for valid options.

 chaps_debug [start|stop|<log_level>]
  Sets the chapsd logging level.  No arguments will start verbose logging.

 connectivity
  Shows connectivity status.  "connectivity help" for more details

 experimental_storage < status | enable | disable >
  Enable or disable experimental storage features.

 ff_debug [<tag_expr>] [--help] [--list_valid_tags] [--reset]
  Add and remove flimflam debugging tags.

 inputcontrol
  Manually adjust advanced touchpad and mouse settings. Run
  "inputcontrol -h" for full list of options.

 memory_test
  Performs extensive memory testing on the available free memory.

 modem <command> [args...]
  Interact with the 3G modem. Run "modem help" for detailed help.

 modem_set_carrier carrier-name
  Configures the modem for the specified carrier.

 network_diag [--date] [--flimflam] [--link] [--show-macs] [--wifi] [--help]
   [--wifi-mon] <host>
  A function that performs a suite of network diagnostics.  Saves a copy
  of the output to your download directory.

 network_logging <wifi | cellular | ethernet>
  A function that enables a predefined set of tags useful for
  debugging the specified device.

 p2p_update [enable|disable]
  Enables or disables the peer-to-peer (P2P) sharing of updates over the local
  network. This will both, attempt to get updates from other peers in the
  network and share the downloaded updates with them. Run this command without
  arguments to see the current state.

 rlz < status | enable | disable >
  Enable or disable RLZ.

 rollback
  Attempt to rollback to the previous update cached on your system. Only
  available on non-stable channels and non-enterprise enrolled devices. Please
  note that this will powerwash your device.

 route [-n] [-6]
  Display the routing tables.

 set_apn [-n <network-id>] [-u <username>] [-p <password>] <apn>
  Set the APN to use when connecting to the network specified by <network-id>.
  If <network-id> is not specified, use the network-id of the currently
  registered network.

 set_apn -c
  Clear the APN to be used, so that the default APN will be used instead.

 set_arpgw <true | false>
  Turn on extra network state checking to make sure the default gateway
  is reachable.

 set_cellular_ppp [-u <username>] [-p <password>]
  Set the PPP username and/or password for an existing cellular connection.
  If neither -u nor -p is provided, show the existing PPP username for
  the cellular connection.

 set_cellular_ppp -c
  Clear any existing PPP username and PPP password for an existing cellular
  connection.

 set_time [<time string>]
  Sets the system time if the the system has been unable to get it from the
  network.  The <time string> uses the format of the GNU coreutils date command.

 sound <command> <argument>
  Low level sound configuration.  Can be used to play/record audio samples
  and enable beam forming on Pixel.
  "sound beamforming <on|off>" will enable/disable the feature.
  "sound record [duration]" will start recording.
  "sound play <filename>" will play the recorded audio samples.

 storage_status
  Reads storage device SMART health status, vendor attributes, and error log.

 storage_test_1
  Performs a short offline SMART test.

 storage_test_2
  Performs an extensive readability test.

 syslog <message>
  Logs a message to syslog.

 time_info
  Returns the current synchronization state for the time service.

 tracepath [-n] <destination>[/port]
  Trace the path/route to a network host.

 update_over_cellular [enable|disable]
  Enables or disables the auto updates over cellular networks. Run without
  arguments to see the current state.

 upload_crashes
  Uploads available crash reports to the crash server.

 wpa_debug [<debug_level>] [--help] [--list_valid_level] [--reset]
  Set wpa_supplicant debugging level.

 xset m [acc_mult[/acc_div] [thr]]
 xset m default
  Tweak the mouse acceleration rate.

 xset r rate [delay [rate]]
  Tweak autorepeat rates.  The delay is the number of milliseconds before
  autorepeat starts.  The rate is the number of repeats per second.
 xset r [keycode] <on|off>
  Turn autorepeat on/off.  If keycode is specified, it affects only that
  key.  If not specified, it affects global behavior.
'

INTRO_TEXT="Welcome to crosh, the Chrome OS developer shell.

If you got here by mistake, don't panic!  Just close this tab and carry on.

Type 'help' for a list of commands.
"

CHROMEOS_INSTALL=/usr/sbin/chromeos-install

load_extra_crosh() {
  # Keep `local` decl split from assignment so return code is checked.
  local crosh_dir devmode removable src

  crosh_dir=$(dirname "$0")

  if [ -e /usr/share/misc/chromeos-common.sh ]; then
    . "/usr/share/misc/chromeos-common.sh" || exit 1

    src=$(get_block_dev_from_partition_dev $(rootdev -s))
    if [ "$(cat /sys/block/${src#/dev/}/removable)" = "1" ]; then
      removable=1
    fi
  fi

  case "${removable}: $* " in
  1:*|\
  *:*" --usb "*)
    . "${crosh_dir}/crosh-usb"
    ;;
  esac

  # Force dev behavior on dev images.
  if type crossystem >/dev/null 2>&1; then
    crossystem "cros_debug?1"
    devmode=$((!$?))
  else
    echo "Could not locate 'crossystem'; assuming devmode is off."
  fi

  case "${devmode}: $* " in
  1:*|\
  *:*" --dev "*)
    . "${crosh_dir}/crosh-dev"
    ;;
  esac
}

shell_read() {
  local prompt="$1"
  shift

  if [ "$IS_BASH" -eq "1" ]; then
    # In bash, -e gives readline support.
    read -p "$prompt" -e $@
  else
    read -p "$prompt" $@
  fi
}

shell_history() {
  if [ "$IS_BASH" -eq "1" ]; then
    # In bash, the history builtin can be used to manage readline history
    history $@
  fi
}

cmd_help() (
  echo "$HELP"
)

cmd_help_advanced() (
  echo "$HELP_ADVANCED"
)

# We move the trailing brace to the next line so that we avoid the style
# checker from rejecting the use of braces.  We cannot use subshells here
# as we want the set the exit variable in crosh itself.
# http://crbug.com/318368
cmd_exit()
{
  exit='y'
}

check_dforward() {
  # Matches a port number between 8000 and 8999.
  expr "$1" : '^8[0-9][0-9][0-9]$' > /dev/null
}

check_forward() {
  # Matches three things, separated by ':':
  # A port number, between 8000 and 8999;
  # A hostname
  # A port number, unrestricted
  expr "$1" : '^8[0-9][0-9][0-9]:[[:alnum:]][-[:alnum:].]*:[1-9][0-9]*$' \
      > /dev/null
}

check_serveraliveinterval() {
  # Matches a number of seconds.
  expr "$1" : '^[0-9][0-9]*$' > /dev/null;
}

check_keyfile() {
  # Allow files in /home/chronos/user, /media. Note that we *do* allow .., so
  # this isn't actually a security barrier, just a molly-guard to keep users
  # from putting keys in insecure storage.
  if [ ! -f "$1" -a ! -f "$HOME/Downloads/$1" ]; then
    return 1;
  fi
  if [ ! -r "$1" -a ! -r "$HOME/Downloads/$1" ]; then
    return 1;
  fi
  (expr "$1" : "^$HOME" > /dev/null) || \
  (expr "$1" : '^/media/' > /dev/null) || \
  (expr "$1" : '^[^/]*$' > /dev/null)
}

cmd_set_time() (
  local spec="$*"
  if [ -z "${spec}" ]; then
    echo "A date/time specification is required."
    echo "E.g., set_time 10 February 2012 11:21am"
    echo "(Remember to set your timezone in Settings first.)"
    return
  fi
  local sec status
  sec=$(date +%s --date="${spec}" 2>&1)
  status=$?
  if [ ${status} -ne 0 -o -z "${sec}" ]; then
    echo "Unable to understand the specified time:"
    echo "${sec}"
    return
  fi
  local reply
  reply=$(dbus-send --system --type=method_call --print-reply \
    --dest=org.torproject.tlsdate /org/torproject/tlsdate \
    org.torproject.tlsdate.SetTime "int64:$((sec))" 2>/dev/null)
  status=$?
  if [ ${status} -ne 0 ]; then
    echo "Time not set. Unable to communicate with the time service."
    return
  fi
  # Reply format: <dbus response header>\n    uint32 <code>\n
  local code
  code=$(echo "${reply}" | sed -n -e '$s/.*uint32 \([0-9]\).*/\1/p')
  case "${code}" in
  0)
    echo "Time has been set."
    ;;
  1)
    echo "Requested time was invalid (too large or too small): ${sec}"
    ;;
  2)
    echo "Time not set. Network time cannot be overriden."
    ;;
  3)
    echo "Time not set. There was a communication error."
    ;;
  *)
    echo "An unexpected response was received: ${code}"
    echo "Details: ${reply}"
  esac
)

cmd_sound() (
  case "$1" in
  "beamforming")
    card=$(aplay -l | egrep '^card .*\bCA0132\b' | sed 's/card \([0-9]\+\).*/\1/')
    if [ -z "$card" ]; then
      echo "No supported card found"
      return 1
    fi
    if [ "$2" != "on" -a "$2" != "off" ]; then
      echo "invalid option $2."
      echo "Valid choices: on, off."
      return 1
    else
      amixer -c"$card" cset name='"Voice Focus Capture Switch"' "$2"
    fi
    ;;
  "play")
    if [ -z "$2" ]; then
      echo "Please specify the audio sample file name."
      return 1
    fi
    echo "playing $2"
    cras_test_client --playback_file "$2"
    ;;
  "record")
    local record_time=0
    if [ $# -lt 2 ]; then
      record_time=30
    else
      record_time="$2"
    fi
    case ${record_time} in
    ""|*[!0-9]*)
      echo "Recording duration has to be numeric."
      return 1
    esac
    if [ ${record_time} -gt 30 -o ${record_time} -lt 1 ]; then
      echo "Recording duration has to be in between 1-30."
      return 1
    fi

    local filename="${HOME}/Downloads/audio_$(date '+%Y%m%d%H%M%S')"
    echo "Recording..."
    cras_test_client --capture_file ${filename} --duration_seconds "${record_time}"
    echo "Audio samples recorded at ${filename}"
    ;;
  *)
    echo "Usage:"
    echo "  sound beamforming <on/off>"
    echo "  sound record [duration]"
    echo "  sound play <filename>"
    return 1
    ;;
  esac
)

check_ssh_host() {
  check_hostname "$1" || check_ipv6 "$1";
}

cmd_ssh() (
  local user=""
  local host=""
  local port="22"
  local idfile=""
  local dforwards=""
  local forwards=""
  local nocmd=""
  local serveraliveinterval=""
  local line
  local cmd
  local params
  local exit
  local id=$(uuidgen)

  local defopts="-e none -F /etc/ssh/ssh_config"

  if [ ! -z "$1" ]; then
    # Sets the username, host, and optionally port directly.  We accept:
    # 'user host', 'user host port', 'user@host', or 'user@host port'.
    local at_pos=$(expr index "$1" '@')
    if [ $at_pos = 0 ]; then
      user="$1"
      host="$2"
      if [ ! -z "$3" ]; then
        port="$3"
      fi
    else
      user=$(substr "$1" "0" $(expr "$at_pos" - 1))
      host=$(substr "$1" "$at_pos")
      if [ ! -z "$2" ]; then
        port="$2"
      fi
    fi
  fi

  if [ -z "$user" -o -z "$host" ]; then
    while [ 1 ]; do
      if ! shell_read "ssh> " line; then
        echo;
        exit=1;
        break
      fi

      local space_pos=$(expr index "$line" ' ')
      if [ $space_pos = 0 ]; then
        cmd="$line"
        params=""
      else
        cmd=$(substr "$line" "0" "$space_pos")
        params=$(substr "$line" "$space_pos")
      fi

      case "$cmd" in

      key)
        if check_keyfile "$params"; then
          mkdir -p -m 600 "$HOME/.ssh";
          trap "rm -f $HOME/.ssh/key-$id" EXIT;
          (cd "$HOME/Downloads" ; cp -- "$params" "$HOME/.ssh/key-$id")
          chmod 600 "$HOME/.ssh/key-$id"
          idfile="-i $HOME/.ssh/key-$id"
        else
          echo "File '$params' is not a valid key file. Key files must reside"
          echo "under /media or $HOME. Key files in the Downloads directory may"
          echo "be specified with an unqualified name."
        fi
        ;;

      dynamic-forward)
        if ! check_dforward "$params"; then
          echo "Invalid forward '$params'."
          echo "Note that the local port number must be in 8000-8999."
        else
          dforwards="$dforwards -D$params"
        fi
        ;;

      forward)
        if ! check_forward "$params"; then
          echo "Invalid forward '$params'"
          echo "Note that the local port number must be in 8000-8999."
        else
          forwards="$forwards -L$params"
        fi
        ;;

      nocmd)
        nocmd="-N"
        ;;

      server-alive-interval)
        if ! check_serveraliveinterval "$params"; then
          echo "Invalid ServerAliveInterval '$params'."
        else
          serveraliveinterval="-oServerAliveInterval=$params"
        fi
        ;;

      port)
        if ! check_digits "$params"; then
          echo "Invalid port '$params'"
        else
          port="$params"
        fi
        ;;

      user)
        user="$params"
        ;;

      host)
        host="$params"
        ;;

      exit)
        exit=1
        break
        ;;

      connect)
        break
        ;;

      *)
        echo "connect                       - connect"
        echo "dynamic-forward port          - dynamic socks proxy (-D)"
        echo "forward port:host:port        - static port forward (-L)"
        echo "help                          - this"
        echo "host <hostname>               - remote hostname"
        echo "key <file>                    - sets private key to use (-i)"
        echo "nocmd                         - don't execute command (-N)"
        echo "port <num>                    - port on remote host (-p)"
        echo "server-alive-interval <num>   - set ServerAliveInterval option"
        echo "exit                          - exit ssh subsystem"
        echo "user <username>               - username on remote host"
        echo "Note that this program can only bind local ports in the range"
        echo "8000-8999, inclusive."
        ;;
      esac
    done
  fi

  if [ -z "$exit" ]; then
    if [ -z "$user" ]; then
      echo "No username given."
    elif [ -z "$host" ]; then
      echo "No host given."
    elif ! check_username "$user"; then
      echo "Invalid username '$user'"
    elif ! check_ssh_host "$host"; then
      echo "Invalid hostname '$host'"
    else
      ssh $defopts $idfile $dforwards $forwards $nocmd $serveraliveinterval \
          -p "$port" -l "$user" "$host"
    fi
  fi

  if [ -n "$idfile" ]; then
    rm "$HOME/.ssh/key-$id";
    trap - EXIT;
  fi
)

cmd_ssh_forget_host() (
  local known_hosts="$(readlink -f $HOME/.ssh/known_hosts)"

  # Test that the known_hosts is a regular file and is not 0 length.
  if [ ! -f "$known_hosts" -o ! -s "$known_hosts" ]; then
    echo "No known hosts."
    return
  fi

  local count="$(cat "$known_hosts" | wc -l)"

  # Print an indexed list of the known hosts.
  echo "Known hosts:"
  awk '{ print " " NR ") " $1; }' "$known_hosts"

  local hostno

  while true; do
    LINE_=""
    shell_read "Please select a host to forget [1-$count]: " LINE_
    if [ -z "$LINE_" ]; then
      echo "Aborting."
      return
    fi

    # Extract the numbers from the user input.
    hostno="$(echo $LINE_ | sed 's/[^[:digit:]]*//g')"

    # If they typed a number...
    if [ -n "$hostno" ]; then
      # And it was in the proper range...
      if [ $hostno -gt 0 -a $hostno -le $count ]; then
        # Then we can stop asking for input.
        break
      fi
    fi

    echo "Invalid selection.  Please enter a number between 1 and $count."
  done

  local trimmed="$(awk -v I="$hostno" 'NR != I { print }' "$known_hosts")"
  echo "$trimmed" > "$known_hosts"
  chmod 644 "$known_hosts"
)

cmd_swap() (
  local swap_enable_file="/home/chronos/.swap_enabled"

  case "$1" in
  "enable")
    if [ -z "$2" ]; then
      # remove file in case root owns it
      rm -f $swap_enable_file
      touch $swap_enable_file
    elif ! echo "$2" | grep -q "^[0-9]*$"; then
      echo "$2 is not a valid integer literal"
      cmd_swap usage
      return 1
    elif [ "$2" -ne 500 -a \
           "$2" -ne 1000 -a \
           "$2" -ne 2000 -a \
           "$2" -ne 3000 -a \
           "$2" -ne 4000 -a \
           "$2" -ne 4500 -a \
           "$2" -ne 6000 ]; then
      echo "invalid size $2."
      echo "Valid choices: 500, 1000, 2000, 3000, 4000, 4500 or 6000."
      cmd_swap usage
      return 1
    else
      # remove file in case root owns it
      rm -f $swap_enable_file
      echo "$2" > $swap_enable_file
      echo "You have selected a size of $2 MB for compressed swap."
      echo "Your choice may affect system performance in various ways."
      echo "Presumably you know what you are doing."
    fi
    echo "Swap will be ON at next reboot."
    ;;
  "disable")
    # remove file in case root owns it
    rm -f $swap_enable_file
    echo 0 > $swap_enable_file
    echo "You are turning compressed swap off."
    echo "This may impact system performance in various ways."
    echo "Presumably you know what you are doing."
    echo "Swap will be OFF at next reboot."
    ;;
  "status")
    /sbin/swapon -s
    ;;
  *)
    echo "Usage:"
    echo "  swap status          print swap device max and current size (if on)"
    echo "  swap enable [<optional swap size (MB)>]"
    echo "  swap disable"
    echo "NOTE: Compressed swap is ON by default.  No tweaking is necessary."
    echo "NOTE: These controls are available for debugging/testing only."
    return 1
    ;;
  esac
)

cmd_experimental_storage() (
  local enable=""
  if [ -z "$1" ]; then
    echo "Usage: experimental_storage < status | enable | disable >"
    return 1
  fi

  case "$1" in
    "status")
      ;;
    "enable")
      enable="true"
      ;;
    "disable")
      enable="false"
      ;;
    *)
      echo "Invalid parameter: $1"
      return 1
      ;;
  esac

  if [ -z "$enable" ]; then
    enable=$( dbus-send --system --type=method_call --print-reply \
        --dest=org.chromium.CrosDisks /org/chromium/CrosDisks \
        org.freedesktop.DBus.Properties.Get string:org.chromium.CrosDisks \
        string:ExperimentalFeaturesEnabled 2>/dev/null )
    if [ $? -eq 0 ]; then
      if echo "$enable" | grep -iqs "true"; then
        echo "Experimental storage features are currently enabled."
      else
        echo "Experimental storage features are currently disabled."
      fi
    else
      echo "Could not determine the status of experimental storage features."
    fi
  else
    if dbus-send --system --type=method_call --print-reply \
        --dest=org.chromium.CrosDisks /org/chromium/CrosDisks \
        org.freedesktop.DBus.Properties.Set \
        string:org.chromium.CrosDisks string:ExperimentalFeaturesEnabled \
        variant:boolean:${enable} >/dev/null 2>&1; then
      echo "Experimental storage features have been ${1}d."
    else
      echo "Could not $1 experimental storage features."
    fi
  fi
)

cmd_time_info() (
  echo "Last time synchronization information:"
  dbus-send --system --type=method_call --print-reply \
      --dest=org.torproject.tlsdate /org/torproject/tlsdate \
      org.torproject.tlsdate.LastSyncInfo 2>/dev/null |
    sed -n \
        -e 's/boolean/network-synchronized:/p' \
        -e 's/string/last-source:/p' \
        -e 's/int64/last-synced-time:/p'
)

cmd_bt_console() (
  if [ -x /usr/bin/bluetoothctl ]; then
    /usr/bin/bluetoothctl "${1:+--agent=$1}"
  else
    echo "Bluetooth console not available"
  fi
)

cmd_ff_debug() (
  /usr/bin/ff_debug "$@"
)

cmd_wpa_debug() (
  /usr/bin/wpa_debug "$@"
)

cmd_set_arpgw() (
  /usr/bin/set_arpgw "$@"
)

cmd_network_logging() (
  if [ -n "$1" ]; then
    case "$1" in
      "wifi")
        echo; /usr/bin/ff_debug service+wifi+inet+device+manager
        echo; /usr/bin/wpa_debug msgdump
        echo; /usr/bin/modem set-logging info
        ;;
      "cellular")
        echo; /usr/bin/ff_debug service+cellular+modem+device+manager
        echo; /usr/bin/wpa_debug info
        echo; /usr/bin/modem set-logging debug
        ;;
      "ethernet")
        echo; /usr/bin/ff_debug service+ethernet+device+manager
        echo; /usr/bin/wpa_debug info
        echo; /usr/bin/modem set-logging info
        ;;
      "--help")
        echo "Usage: network_logging <wifi | cellular | ethernet>"
        ;;
      *)
        echo "Invalid parameter $1"
        ;;
    esac
  else
    echo "Missing parameter wifi | cellular | ethernet"
  fi
)

cmd_network_diag() (
  /usr/bin/network_diagnostics "$@"
)

debugd() {
  local method="$1"; shift
  dbus-send --system --print-reply --fixed --dest=org.chromium.debugd \
      /org/chromium/debugd "org.chromium.debugd.$method" "$@"
}

cmd_ping() (
  local option="dict:string:variant:"

  # NB: use printf to avoid echo interpreting -n
  while [ "$(printf '%s' "$1" | cut -c1)" = "-" ]; do
    # Do just enough parsing to filter/map options; we
    # depend on ping to handle final validation
    if [ "$1" = "-i" ]; then
      shift; option="${option}interval,int32:$1,"
    elif [ "$1" = "-c" ]; then
      shift; option="${option}count,int32:$1,"
    elif [ "$1" = "-W" ]; then
      shift; option="${option}waittime,int32:$1,"
    elif [ "$1" = "-s" ]; then
      shift; option="${option}packetsize,int32:$1,"
    elif [ "$1" = "-n" ]; then
      option="${option}numeric,boolean:true,"
    elif [ "$1" = "-b" ]; then
      option="${option}broadcast,boolean:true,"
    else
      echo "Unknown option: $1"
      return 1
    fi

    shift
  done

  if [ -z "$1" ]; then
    echo "Missing parameter: destination"
    return 1
  fi

  local dest="$1"
  if [ "$dest" = "gw" ]; then
    # Convenient shorthand for the next-hop gateway attached
    # to the default route; this means if you have a host named
    # "gw" then you'll need to specify a FQDN or IP address.
    dest=$(/sbin/route -n | awk '$1 == "0.0.0.0" { print $2; }')
    if [ -z "$dest" ]; then
      echo "Cannot determine primary gateway; routing table is:"
      cmd_route -n
      return 1
    fi
  fi

  option=$(echo "$option" | sed 's/,$//')

  fifo="$(mk_fifo)"
  debugd PingStart "fd:1" "string:$dest" "$option" 2>&1 > "$fifo" &
  read pid < "$fifo"
  echo "pid: $pid"
  (while read line; do echo "$line"; done) < "$fifo"
  debugd PingStop "string:$pid"
  if [ $? -ne 0 ]; then
    echo "Can't stop ping"
    return 1
  fi
  rm -rf "$(dirname ${fifo})"
)

cmd_progressive_scan() (
  local flag_file="/home/chronos/.progressive_scan"
  local enabled=0
  local changed=0
  if [ -r ${flag_file} ]; then
    enabled=1
  fi
  if [ -z "$1" ]; then
    if [ ${enabled} -eq 1 ]; then
      echo "Currently enabled"
    else
      echo "Currently disabled"
    fi
    return
  fi
  if [ "$1" = "on" ]; then
    if [ $enabled -eq 0 ]; then
      changed=1;
    fi
    touch "${flag_file}"
  else
    if [ $enabled -eq 1 ]; then
      changed=1
    fi
    rm -f "${flag_file}"
  fi
  if [ $changed -eq 1 ]; then
    echo "You must reboot (or restart shill) for this to take effect."
  else
    echo "No change."
  fi
)

cmd_chaps_debug() (
  local level=${1:--2}
  if [ "$1" = "stop" ]; then
    level=0
  fi
  if [ "$1" = "start" ]; then
    level=-2
  fi
  rm -f /home/chronos/.chaps_debug*
  if [ "${level}" = "-1" ]; then
    touch /home/chronos/.chaps_debug_1
  elif [ "${level}" = "-2" ]; then
    touch /home/chronos/.chaps_debug_2
  fi
  /usr/bin/chaps_client --set_log_level=${level} 2> /dev/null
  if [ $? -eq 0 ]; then
    echo "Logging level set to ${level}."
  else
    echo "Failed to set logging level."
  fi
)

cmd_route() (
  local option="dict:string:variant:"

  # NB: use printf to avoid echo interpreting -n
  while [ "$(printf '%s' "$1" | cut -c1)" = "-" ]; do
    if [ "$1" = "-n" ]; then
      option="${option}numeric,boolean:true,"
    elif [ "$1" = "-6" ]; then
      option="${option}v6,boolean:true,"
    else
      echo "Unknown option: $1"
      return 1
    fi

    shift
  done
  option=$(echo "$option" | sed 's/,$//')

  debugd GetRoutes "$option"
)

cmd_tracepath() (
  local option="dict:string:variant:"

  # NB: use printf to avoid echo interpreting -n
  while [ "$(printf '%s' "$1" | cut -c1)" = "-" ]; do
    if [ "$1" = "-n" ]; then
      option="${option}numeric,boolean:true,"
    else
      echo "Unknown option: $1"
      return 1
    fi

    shift
  done

  option=$(echo "$option" | sed 's/,$//')

  if [ -z "$1" ]; then
    echo "Missing parameter: destination"
    return 1
  fi

  fifo="$(mk_fifo)"
  debugd TracePathStart "fd:1" "string:$1" "$option" 2>&1 > "$fifo" &
  read pid < "$fifo"
  echo "pid: $pid"
  (while read line; do echo "$line"; done) < "$fifo"
  debugd TracePathStop "string:$pid"
  if [ $? -ne 0 ]; then
    echo "Can't stop tracepath"
    return 1
  fi
  rm -rf "$(dirname ${fifo})"
)

cmd_top() (
  # -s is "secure" mode, which disables kill, renice, and change display/sleep
  # interval.
  top -s
)

cmd_modem() (
  /usr/bin/modem "$@"
)

cmd_modem_set_carrier() (
  /usr/bin/modem set-carrier "$@"
)

cmd_set_apn() (
  /usr/bin/set_apn "$@"
)

cmd_set_cellular_ppp() (
  /usr/bin/set_cellular_ppp "$@"
)

cmd_connectivity() (
  /usr/bin/connectivity "$@"
)

cmd_autest() (
  local omaha_url="autest"

  if [ "$1" = "-scheduled" ]; then
   # pretend that this is a scheduled check as opposed to an user-initiated
   # check for testing features that get enabled only on scheduled checks.
   omaha_url="autest-scheduled"
  fi

  echo "Calling update_engine_client with omaha_url = $omaha_url"
  /usr/bin/update_engine_client --omaha_url $omaha_url
)

cmd_p2p_update() (
  case "$1" in
    "enable")
      param="-p2p_update=yes"
      ;;

    "disable")
      param="-p2p_update=no"
      ;;

    "")
      param=""
      ;;

    *)
      echo "Usage: p2p_update [enable|disable]
  Enables or disables the peer-to-peer (P2P) sharing of updates over the local
  network. This will both, attempt to get updates from other peers in the
  network and share the downloaded updates with them. Run this command without
  arguments to see the current state."
      return 1
      ;;
  esac
  /usr/bin/update_engine_client $param -show_p2p_update
)

cmd_rollback() (
  if /usr/bin/update_engine_client --rollback; then
    echo "Rollback attempt succeeded -- after a couple minutes you will" \
         "get an update available and you should reboot to complete rollback."
  else
    echo "Rollback attempt failed. Check chrome://system for more information."
  fi
)

cmd_update_over_cellular() (
  case "$1" in
    "enable")
      param="-update_over_cellular=yes"
      echo "When available, auto-updates download in the background any time " \
           "the computer is powered on.  Note: this may incur additional " \
           "cellular charges, including roaming and/or data charges, as per " \
           "your carrier arrangement."
      ;;

    "disable")
      param="-update_over_cellular=no"
      ;;

    "")
      param=""
      ;;

    *)
      echo "Usage: update_over_cellular [enable|disable]
  Enables or disables the auto updates over cellular networks. Run without
  arguments to see the current state."
      return 1
      ;;
  esac
  /usr/bin/update_engine_client $param -show_update_over_cellular
)

cmd_upload_crashes() (
  debugd UploadCrashes
  echo "Check chrome://crashes for status updates"
)

cmd_tpcontrol() (
  echo "tpcontrol is deprecated. Please use inputcontrol instead."
)

cmd_try_touch_experiment() (
  /usr/sbin/try_touch_experiment "$@"
)

cmd_inputcontrol() (
  /opt/google/input/inputcontrol "$@"
)

cmd_rlz() (
  local flag_file="$HOME/.rlz_disabled"
  local enabled=1
  local changed=0
  if [ -r "${flag_file}" ]; then
    enabled=0
  fi
  case "$1" in
    "status")
      if [ $enabled -eq 1 ]; then
        echo "Currently enabled"
      else
        echo "Currently disabled"
      fi
      return
      ;;

    "enable")
      if [ $enabled -eq 0 ]; then
        changed=1
      fi
      rm -f "${flag_file}"
      ;;

    "disable")
      if [ $enabled -eq 1 ]; then
        changed=1
      fi
      touch "${flag_file}"
      ;;

    *)
      echo "Usage: rlz < status | enable | disable >"
      return 1
      ;;
  esac
  if [ $changed -eq 1 ]; then
    echo "You must reboot for this to take effect."
  else
    echo "No change."
  fi
)

cmd_syslog() (
  logger -t crosh -- "$*"
)

cmd_xset() (
  case $1 in
  m|r|-r) ;;
  *) set -- ;; # Gets us a usage string.
  esac
  xset-mini "$@"
)

mk_fifo() {
  # We want C-c to terminate the running test so that the UI stays the same.
  # Therefore, create a fifo to direct the output of the test to, and have a
  # subshell read from the fifo and emit to stdout. When the subshell ends (at a
  # C-c), we stop the test and clean up the fifo.
  # no way to mktemp a fifo, so make a dir to hold it instead
  dir=$(mktemp -d "/tmp/crosh-test-XXXXXXXXXX")
  if [ $? -ne 0 ]; then
    echo "Can't create temporary directory"
    return 1
  fi
  fifo="${dir}/fifo"
  if ! mkfifo "$fifo"; then
    echo "Can't create fifo at $fifo"
    return 1
  fi

  echo "$fifo"
}

cmd_storage_test_1() (
  option="$1"

  debugd Smartctl "string:abort_test" >/dev/null

  test=$(debugd Smartctl "string:short_test")
  if [ "$option" != "-v" ]; then
    echo "$test" | sed -n '1p;2p'
    echo ""
    echo "$test" | grep "Please wait"
  else
    echo "$test"
  fi

  echo ""

  while debugd Smartctl "string:capabilities" |
        grep -q "of test remaining"; do
    true
  done

  result=$(debugd Smartctl "string:selftest")
  if [ "$option" != "-v" ]; then
    echo "$result" | grep -e "Num" -e "# 1"
  else
    echo "$result"
  fi

  debugd Smartctl "string:abort_test" >/dev/null
)

cmd_storage_test_2() (
  fifo="$(mk_fifo)"
  pid_file="$(mktemp)"
  debugd BadblocksStart "fd:1" 2>&1 > "$fifo" &
  (while read line; do echo "$line" | tee -a "$pid_file"; done) < "$fifo"
  read -r pid < "$pid_file"
  rm -f "$pid_file"

  debugd BadblocksStop "string:$pid"
  if [ $? -ne 0 ]; then
    echo "Can't stop badblocks"
    return 1
  fi
  rm -rf "$(dirname ${fifo})"
)

cmd_storage_status() (
  option="$1"

  result=$(debugd Smartctl "string:health")
  if [ "$option" != "-v" ]; then
    echo "$result" | sed -n '1p;2p'
    echo ""
    echo "$result" | sed -n '/START OF READ/,$p' | sed -n '2,$p'
  else
    echo "$result"
  fi

  echo ""

  result=$(debugd Smartctl "string:error")
  if [ "$option" != "-v" ]; then
    echo "$result" | sed -n '/START OF READ/,$p' | sed -n '3,$p'
  else
    echo "$result"
  fi

  echo ""

  result=$(debugd Smartctl "string:attributes")
  if [ "$option" != "-v" ]; then
    echo "$result" | sed -n '/START OF READ/,$p' | sed -n '3,$p'
  else
    echo "$result"
  fi
)

cmd_memory_test() (
  # Getting total free memory in KB.
  mem=$(cat /proc/meminfo | grep MemFree | tr -s " " | cut -d" " -f 2)

  # Converting to MB.
  mem=$(($mem / 1024))

  # Giving OS 200MB free memory before hogging the rest of it.
  mem=$(($mem - 200))

  fifo="$(mk_fifo)"
  pid_file="$(mktemp)"
  debugd MemtesterStart "fd:1" "uint32:$mem" 2>&1 > "$fifo" &
  (while read line; do echo "$line" | tee -a "$pid_file"; done) < "$fifo"
  read -r pid < "$pid_file"
  rm -f "$pid_file"

  debugd MemtesterStop "string:$pid"
  if [ $? -ne 0 ]; then
    echo "Can't stop memtester"
    return 1
  fi
  rm -rf "$(dirname ${fifo})"
)

cmd_battery_test() (
  test_length="$1"

  if [ -z "$test_length" ]; then
    echo "No test length specified. Defaulting to 300s"
    test_length=300
  fi

  # Ensuring that the parameter is an int.
  if ! echo "${test_length}" | egrep -q '^[0-9]+$'; then
    echo "You provided invalid test length. Exiting..."
    return 1
  fi

  ac="$(ls /sys/class/power_supply/*/online)"
  ac="$(dirname ${ac})"

  bat="/sys/class/power_supply/*/technology"

  # Checking if battery is present in the system.
  if ! ls ${bat} >/dev/null 2>&1; then
    echo "No battery found. Insert the battery and reboot the device."
    return 1
  else
    # Battery exists. Getting its path.
    bat="$(ls ${bat})"
    bat="$(dirname ${bat})"
  fi

  # Figuring out if battery gauge measures energy left or charge.
  if ls ${bat}/energy* >/dev/null 2>&1; then
    meter="energy"
  elif ls ${bat}/charge* >/dev/null 2>&1; then
    meter="charge"
  else
    echo "Could not figure out the way to get battery status"
    return 1
  fi

  ac_online="$(cat ${ac}/online)"
  bat_status="$(cat ${bat}/status)"
  bat_now="$(cat ${bat}/${meter}_now)"
  bat_full="$(cat ${bat}/${meter}_full)"
  bat_full_design="$(cat ${bat}/${meter}_full_design)"

  echo "Battery is $bat_status ($((${bat_now}*100/${bat_full}))% left)"
  echo "Battery health: $((${bat_full}*100/${bat_full_design}))%"

  if [ "$bat_status" != "Discharging" ]; then
    echo "Please make sure the power supply is unplugged and retry the test"
    return 1
  fi

  echo "Please wait.."

  sleep "$test_length"

  bat_after="$(cat ${bat}/${meter}_now)"
  bat_diff="$((${bat_now}-${bat_after}))"
  bat_diff="$((${bat_diff}*100))" #this allows to have only 2 decimal digits

  bat_perc="$((${bat_diff}*100/${bat_full}))"

  bat_perc_int="$((${bat_perc}/100))"
  bat_perc_dec="$((${bat_perc}%100))"

  echo "Battery discharged ${bat_perc_int}.${bat_perc_dec}% in ${test_length}s"
)

cmd_dump_emk() (
  /usr/sbin/cryptohome --action=tpm_attestation_key_status \
                       --name=attest-ent-machine
)

substr() {
  local str="$1"
  local start="$2"
  local end="$3"

  if [ "$IS_BASH" = "1" ]; then
    # NB: use printf to avoid echo interpreting -n
    if [ -z "$end" ]; then
      printf '%s\n' ${str:$start}
    else
      printf '%s\n' ${str:$start:$end}
    fi
    return
  fi

  start=$(expr "$start" + 1)

  if [ ! -z "$end" ]; then
    end=$(expr "$end" - 1)
  fi

  echo "$str" | cut -c${start}-${end}
}

dispatch() {
  local line="$1"
  local command=""
  local params=""

  local space_pos=$(expr index "$line" ' ')

  if [ $space_pos = 0 ]; then
    command=$line
  else
    command=$(substr "$line" "0" "$space_pos")
    command=${command% *}
    params=$(substr "$line" "$space_pos")
  fi

  if ! type "cmd_$command" 2>/dev/null | head -1 | grep -q "function"; then
    echo "Unknown command: '$command'"
  else
    command="cmd_$command"
    $command $params
  fi
}

# Checks that a given string looks like a hostname or IPv4 address (starts
# with an alphanumeric and contains only alphanumeric, '.', or '-'
# characters).
check_hostname() {
  expr "$1" : '^[[:alnum:]][-[:alnum:].]*$' > /dev/null
}

# Checks that a given string could plausibly be an IPv6 address
# (hexadecimal, ':', and '.' characters only, followed by an optional zone
# index consisting of a '%' and a device name).
check_ipv6() {
  echo "$1" | /bin/grep -E -q '^[0-9a-fA-F:.]+(%[a-z0-9]+)?$'
}

# Checks that a given string starts with an alphanumeric, and contains only
# alphanumeric and zero or more of "_:.~%$^\-"
check_username() {
  expr "$1" : '^[[:alnum:]][[:alnum:]_:.~%$^\-]*$' > /dev/null
}

check_digits() {
  expr "$1" : '^[[:digit:]]*$' > /dev/null
}

repl() {
  echo "${INTRO_TEXT}"
  if [ "$IS_BASH" != "1" ]; then
    echo "Sorry, line editing and command history disabled due to" \
      "shell limitations."
  fi

  # This will be set by the 'exit' command to tell us to quit.
  local exit
  while [ -z "${exit}" ]; do
    if shell_read "crosh> " LINE_; then
      if [ ! -z "$LINE_" ]; then
        shell_history -s "$LINE_"
        dispatch "$LINE_"
      fi
    else
      echo
      return 1
    fi
  done
}

main() {
  load_extra_crosh "$@"

  INPUTRC="/usr/share/misc/inputrc.crosh"
  if [ ! -e "${INPUTRC}" ]; then
    # We aren't installed; use local copy for testing.
    INPUTRC="$(dirname "$0")/inputrc.crosh"
  fi
  HISTFILE="${HOME}/.crosh_history"
  shell_history -r "${HISTFILE}"

  # Initialize pseudo completion support.
  if [ ${IS_BASH} -eq 1 ]; then
    local f
    for f in $(declare -F | sed -n '/-f cmd_/s:.*cmd_::p'); do
      # Do not add duplicates to avoid ballooning history.
      grep -qs "^${f}$" $HISTFILE || shell_history -s $f
    done
  fi

  repl

  shell_history -w $HISTFILE
}
main "$@"
